/*
 * Copyright 2000-2017 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.util;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Condition;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import com.intellij.util.containers.JBTreeTraverser;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;

public class ReflectionUtil {
    private static final Logger LOG = Logger.getInstance("#com.intellij.util.ReflectionUtil");

    private ReflectionUtil() {
    }

    @Nullable
    public static Type resolveVariable(@NotNull TypeVariable variable, @NotNull Class classType) {
        return resolveVariable(variable, classType, true);
    }

    @Nullable
    public static Type resolveVariable(@NotNull TypeVariable variable, @NotNull Class classType, boolean resolveInInterfacesOnly) {
        final Class aClass = getRawType(classType);
        int index = ArrayUtilRt.find(aClass.getTypeParameters(), variable);
        if (index >= 0) {
            return variable;
        }

        final Class[] classes = aClass.getInterfaces();
        final Type[] genericInterfaces = aClass.getGenericInterfaces();
        for (int i = 0; i <= classes.length; i++) {
            Class anInterface;
            if (i < classes.length) {
                anInterface = classes[i];
            } else {
                anInterface = aClass.getSuperclass();
                if (resolveInInterfacesOnly || anInterface == null) {
                    continue;
                }
            }
            final Type resolved = resolveVariable(variable, anInterface);
            if (resolved instanceof Class || resolved instanceof ParameterizedType) {
                return resolved;
            }
            if (resolved instanceof TypeVariable) {
                final TypeVariable typeVariable = (TypeVariable) resolved;
                index = ArrayUtilRt.find(anInterface.getTypeParameters(), typeVariable);
                if (index < 0) {
                    LOG.error("Cannot resolve type variable:\n" + "typeVariable = " + typeVariable + "\n" + "genericDeclaration = " +
                            declarationToString(typeVariable.getGenericDeclaration()) + "\n" + "searching in " + declarationToString(anInterface));
                }
                final Type type = i < genericInterfaces.length ? genericInterfaces[i] : aClass.getGenericSuperclass();
                if (type instanceof Class) {
                    return Object.class;
                }
                if (type instanceof ParameterizedType) {
                    return getActualTypeArguments((ParameterizedType) type)[index];
                }
                throw new AssertionError("Invalid type: " + type);
            }
        }
        return null;
    }

    @NotNull
    public static String declarationToString(@NotNull GenericDeclaration anInterface) {
        return anInterface.toString() + Arrays.asList(anInterface.getTypeParameters()) + " loaded by " + ((Class) anInterface).getClassLoader();
    }

    @NotNull
    public static Class<?> getRawType(@NotNull Type type) {
        if (type instanceof Class) {
            return (Class) type;
        }
        if (type instanceof ParameterizedType) {
            return getRawType(((ParameterizedType) type).getRawType());
        }
        if (type instanceof GenericArrayType) {
            //todo[peter] don't create new instance each time
            return Array.newInstance(getRawType(((GenericArrayType) type).getGenericComponentType()), 0).getClass();
        }
        assert false : type;
        return null;
    }

    @NotNull
    public static Type[] getActualTypeArguments(@NotNull ParameterizedType parameterizedType) {
        return parameterizedType.getActualTypeArguments();
    }

    @Nullable
    public static Class<?> substituteGenericType(@NotNull Type genericType, @NotNull Type classType) {
        if (genericType instanceof TypeVariable) {
            final Class<?> aClass = getRawType(classType);
            final Type type = resolveVariable((TypeVariable) genericType, aClass);
            if (type instanceof Class) {
                return (Class) type;
            }
            if (type instanceof ParameterizedType) {
                return (Class<?>) ((ParameterizedType) type).getRawType();
            }
            if (type instanceof TypeVariable && classType instanceof ParameterizedType) {
                final int index = ArrayUtilRt.find(aClass.getTypeParameters(), type);
                if (index >= 0) {
                    return getRawType(getActualTypeArguments((ParameterizedType) classType)[index]);
                }
            }
        } else {
            return getRawType(genericType);
        }
        return null;
    }

    /*@NotNull
    public static List<Field> collectFields(@NotNull Class clazz) {
        List<Field> result = ContainerUtil.newArrayList();
        for (Class c : classTraverser(clazz)) {
            result.addAll(getClassDeclaredFields(c));
        }
        return result;
    }*/

    @NotNull
    public static Field findField(@NotNull Class clazz, @Nullable final Class type, @NotNull final String name) throws NoSuchFieldException {
        Field result = processFields(clazz, new Condition<Field>() {
            @Override
            public boolean value(Field field) {
                return name.equals(field.getName()) && (type == null || field.getType().equals(type));
            }
        });
        if (result != null) return result;

        throw new NoSuchFieldException("Class: " + clazz + " name: " + name + " type: " + type);
    }

    @NotNull
    public static Field findAssignableField(@NotNull Class<?> clazz, @Nullable("null means any type") final Class<?> fieldType, @NotNull final String fieldName) throws NoSuchFieldException {
        Field result = processFields(clazz, new Condition<Field>() {
            @Override
            public boolean value(Field field) {
                return fieldName.equals(field.getName()) && (fieldType == null || fieldType.isAssignableFrom(field.getType()));
            }
        });
        if (result != null) return result;
        throw new NoSuchFieldException("Class: " + clazz + " fieldName: " + fieldName + " fieldType: " + fieldType);
    }

    @Nullable
    private static Field processFields(@NotNull Class clazz, @NotNull Condition<Field> checker) {
        for (Class c : classTraverser(clazz)) {
            Field field = JBIterable.of(c.getDeclaredFields()).find(checker);
            if (field != null) {
                field.setAccessible(true);
                return field;
            }
        }
        return null;
    }

    public static void resetField(@NotNull Class clazz, @Nullable("null means of any type") Class type, @NotNull String name) {
        try {
            resetField(null, findField(clazz, type, name));
        } catch (NoSuchFieldException e) {
            LOG.info(e);
        }
    }

    public static void resetField(@NotNull Object object, @Nullable("null means any type") Class type, @NotNull String name) {
        try {
            resetField(object, findField(object.getClass(), type, name));
        } catch (NoSuchFieldException e) {
            LOG.info(e);
        }
    }

    public static void resetField(@NotNull Object object, @NotNull String name) {
        try {
            resetField(object, findField(object.getClass(), null, name));
        } catch (NoSuchFieldException e) {
            LOG.info(e);
        }
    }

    public static void resetField(@Nullable final Object object, @NotNull Field field) {
        field.setAccessible(true);
        Class<?> type = field.getType();
        try {
            if (type.isPrimitive()) {
                if (boolean.class.equals(type)) {
                    field.set(object, Boolean.FALSE);
                } else if (int.class.equals(type)) {
                    field.set(object, Integer.valueOf(0));
                } else if (double.class.equals(type)) {
                    field.set(object, Double.valueOf(0));
                } else if (float.class.equals(type)) {
                    field.set(object, Float.valueOf(0));
                }
            } else {
                field.set(object, null);
            }
        } catch (IllegalAccessException e) {
            LOG.info(e);
        }
    }

    public static void resetStaticField(@NotNull Class aClass, @NotNull @NonNls String name) {
        resetField(aClass, null, name);
    }

    @Nullable
    public static Method findMethod(@NotNull Collection<Method> methods, @NonNls @NotNull String name, @NotNull Class... parameters) {
        for (final Method method : methods) {
            if (name.equals(method.getName()) && Arrays.equals(parameters, method.getParameterTypes())) {
                method.setAccessible(true);
                return method;
            }
        }
        return null;
    }

    @Nullable
    public static Method getMethod(@NotNull Class aClass, @NonNls @NotNull String name, @NotNull Class... parameters) {
        return findMethod(getClassPublicMethods(aClass, false), name, parameters);
    }

    @Nullable
    public static Method getDeclaredMethod(@NotNull Class aClass, @NonNls @NotNull String name, @NotNull Class... parameters) {
        return findMethod(getClassDeclaredMethods(aClass, false), name, parameters);
    }

    @Nullable
    public static Field getDeclaredField(@NotNull Class aClass, @NonNls @NotNull final String name) {
        return processFields(aClass, new Condition<Field>() {
            @Override
            public boolean value(Field field) {
                return name.equals(field.getName());
            }
        });
    }

    @NotNull
    public static List<Method> getClassPublicMethods(@NotNull Class aClass) {
        return getClassPublicMethods(aClass, false);
    }

    @NotNull
    public static List<Method> getClassPublicMethods(@NotNull Class aClass, boolean includeSynthetic) {
        Method[] methods = aClass.getMethods();
        return includeSynthetic ? Arrays.asList(methods) : filterRealMethods(methods);
    }

    @NotNull
    public static List<Method> getClassDeclaredMethods(@NotNull Class aClass) {
        return getClassDeclaredMethods(aClass, false);
    }

    @NotNull
    public static List<Method> getClassDeclaredMethods(@NotNull Class aClass, boolean includeSynthetic) {
        Method[] methods = aClass.getDeclaredMethods();
        return includeSynthetic ? Arrays.asList(methods) : filterRealMethods(methods);
    }

    @NotNull
    public static List<Field> getClassDeclaredFields(@NotNull Class aClass) {
        Field[] fields = aClass.getDeclaredFields();
        return Arrays.asList(fields);
    }

    @NotNull
    private static List<Method> filterRealMethods(@NotNull Method[] methods) {
        List<Method> result = ContainerUtil.newArrayList();
        for (Method method : methods) {
            if (!method.isSynthetic()) {
                result.add(method);
            }
        }
        return result;
    }

    @Nullable
    public static Class getMethodDeclaringClass(@NotNull Class<?> instanceClass, @NonNls @NotNull String methodName, @NotNull Class... parameters) {
        Method method = getMethod(instanceClass, methodName, parameters);
        return method == null ? null : method.getDeclaringClass();
    }

    public static <T> T getField(@NotNull Class objectClass, @Nullable Object object, @Nullable("null means any type") Class<T> fieldType, @NotNull @NonNls String fieldName) {
        try {
            final Field field = findAssignableField(objectClass, fieldType, fieldName);
            @SuppressWarnings("unchecked") T t = (T) field.get(object);
            return t;
        } catch (NoSuchFieldException e) {
            LOG.debug(e);
            return null;
        } catch (IllegalAccessException e) {
            LOG.debug(e);
            return null;
        }
    }

    public static <T> T getStaticFieldValue(@NotNull Class objectClass, @Nullable("null means any type") Class<T> fieldType, @NotNull @NonNls String fieldName) {
        try {
            final Field field = findAssignableField(objectClass, fieldType, fieldName);
            if (!Modifier.isStatic(field.getModifiers())) {
                throw new IllegalArgumentException("Field " + objectClass + "." + fieldName + " is not static");
            }
            @SuppressWarnings("unchecked") T t = (T) field.get(null);
            return t;
        } catch (NoSuchFieldException e) {
            LOG.debug(e);
            return null;
        } catch (IllegalAccessException e) {
            LOG.debug(e);
            return null;
        }
    }

    // returns true if value was set
    public static <T> boolean setField(@NotNull Class objectClass,
                                       Object object,
                                       @Nullable("null means any type") Class<T> fieldType,
                                       @NotNull @NonNls String fieldName,
                                       T value) {
        try {
            final Field field = findAssignableField(objectClass, fieldType, fieldName);
            field.set(object, value);
            return true;
        } catch (NoSuchFieldException e) {
            LOG.debug(e);
            // this 'return' was moved into 'catch' block because otherwise reference to common super-class of these exceptions (ReflectiveOperationException)
            // which doesn't exist in JDK 1.6 will be added to class-file during instrumentation
            return false;
        } catch (IllegalAccessException e) {
            LOG.debug(e);
            return false;
        }
    }

    public static Type resolveVariableInHierarchy(@NotNull TypeVariable variable, @NotNull Class aClass) {
        Type type;
        Class current = aClass;
        while ((type = resolveVariable(variable, current, false)) == null) {
            current = current.getSuperclass();
            if (current == null) {
                return null;
            }
        }
        if (type instanceof TypeVariable) {
            return resolveVariableInHierarchy((TypeVariable) type, aClass);
        }
        return type;
    }

    @NotNull
    public static <T> Constructor<T> getDefaultConstructor(@NotNull Class<T> aClass) {
        try {
            final Constructor<T> constructor = aClass.getConstructor();
            constructor.setAccessible(true);
            return constructor;
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("No default constructor in " + aClass, e);
        }
    }

    /**
     * Like {@link Class#newInstance()} but also handles private classes
     */
    @NotNull
    public static <T> T newInstance(@NotNull Class<T> aClass) {
        try {
            Constructor<T> constructor = aClass.getDeclaredConstructor();
            try {
                constructor.setAccessible(true);
            } catch (SecurityException e) {
                return aClass.newInstance();
            }
            return constructor.newInstance();
        } catch (Exception e) {
            T t = createAsDataClass(aClass);
            if (t != null) {
                return t;
            }

            ExceptionUtil.rethrow(e);
        }

        // error will be thrown
        //noinspection ConstantConditions
        return null;
    }

    @Nullable
    private static <T> T createAsDataClass(@NotNull Class<T> aClass) {
        // support Kotlin data classes - pass null as default value
        for (Annotation annotation : aClass.getAnnotations()) {
            String name = annotation.annotationType().getName();
            if (!name.equals("kotlin.Metadata") && !name.equals("kotlin.jvm.internal.KotlinClass")) {
                continue;
            }

            Constructor<?>[] constructors = aClass.getDeclaredConstructors();
            Exception exception = null;
            List<Constructor<?>> defaultCtors = new SmartList<Constructor<?>>();
            ctorLoop:
            for (Constructor<?> constructor : constructors) {
                try {
                    try {
                        constructor.setAccessible(true);
                    } catch (Throwable ignored) {
                    }

                    Class<?>[] parameterTypes = constructor.getParameterTypes();
                    for (Class<?> type : parameterTypes) {
                        if (type.getName().equals("kotlin.jvm.internal.DefaultConstructorMarker")) {
                            defaultCtors.add(constructor);
                            continue ctorLoop;
                        }
                    }

                    //noinspection unchecked
                    return (T) constructor.newInstance(new Object[parameterTypes.length]);
                } catch (Exception e) {
                    exception = e;
                }
            }

            for (Constructor<?> constructor : defaultCtors) {
                try {
                    try {
                        constructor.setAccessible(true);
                    } catch (Throwable ignored) {
                    }

                    //noinspection unchecked
                    return (T) constructor.newInstance();
                } catch (Exception e) {
                    exception = e;
                }
            }

            if (exception != null) {
                ExceptionUtil.rethrow(exception);
            }
        }
        return null;
    }

    @NotNull
    public static <T> T createInstance(@NotNull Constructor<T> constructor, @NotNull Object... args) {
        try {
            return constructor.newInstance(args);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Nullable
    public static Class getGrandCallerClass() {
        int stackFrameCount = 3;
        Class callerClass = findCallerClass(stackFrameCount);
        while (callerClass != null && callerClass.getClassLoader() == null) { // looks like a system class
            callerClass = findCallerClass(++stackFrameCount);
        }
        if (callerClass == null) {
            callerClass = findCallerClass(2);
        }
        return callerClass;
    }

    /*public static void copyFields(@NotNull Field[] fields, @NotNull Object from, @NotNull Object to) {
        copyFields(fields, from, to, null);
    }

    public static boolean copyFields(@NotNull Field[] fields, @NotNull Object from, @NotNull Object to, @Nullable DifferenceFilter diffFilter) {
        Set<Field> sourceFields = ContainerUtil.newHashSet(from.getClass().getFields());
        boolean valuesChanged = false;
        for (Field field : fields) {
            if (sourceFields.contains(field)) {
                if (isPublic(field) && !isFinal(field)) {
                    try {
                        if (diffFilter == null || diffFilter.isAccept(field)) {
                            copyFieldValue(from, to, field);
                            valuesChanged = true;
                        }
                    }
                    catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        return valuesChanged;
    }*/

    public static boolean comparePublicNonFinalFields(@NotNull Object first,
                                                      @NotNull Object second) {
        Set<Field> firstFields = ContainerUtil.newHashSet(first.getClass().getFields());
        for (Field field : second.getClass().getFields()) {
            if (firstFields.contains(field)) {
                if (isPublic(field) && !isFinal(field)) {
                    try {
                        if (!Comparing.equal(field.get(first), field.get(second))) {
                            return false;
                        }
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        return true;
    }

    public static void copyFieldValue(@NotNull Object from, @NotNull Object to, @NotNull Field field)
            throws IllegalAccessException {
        Class<?> fieldType = field.getType();
        if (fieldType.isPrimitive() || fieldType.equals(String.class) || fieldType.isEnum()) {
            field.set(to, field.get(from));
        } else {
            throw new RuntimeException("Field '" + field.getName() + "' not copied: unsupported type: " + field.getType());
        }
    }

    private static boolean isPublic(final Field field) {
        return (field.getModifiers() & Modifier.PUBLIC) != 0;
    }

    private static boolean isFinal(final Field field) {
        return (field.getModifiers() & Modifier.FINAL) != 0;
    }

    @NotNull
    public static Class<?> forName(@NotNull String fqn) {
        try {
            return Class.forName(fqn);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    private static class MySecurityManager extends SecurityManager {
        private static final MySecurityManager INSTANCE = new MySecurityManager();

        public Class[] getStack() {
            return getClassContext();
        }
    }

    /**
     * Returns the class this method was called 'framesToSkip' frames up the caller hierarchy.
     * <p>
     * NOTE:
     * <b>Extremely expensive!
     * Please consider not using it.
     * These aren't the droids you're looking for!</b>
     */
    @Nullable
    public static Class findCallerClass(int framesToSkip) {
        try {
            Class[] stack = MySecurityManager.INSTANCE.getStack();
            int indexFromTop = 1 + framesToSkip;
            return stack.length > indexFromTop ? stack[indexFromTop] : null;
        } catch (Exception e) {
            LOG.warn(e);
            return null;
        }
    }

    public static boolean isAssignable(@NotNull Class<?> ancestor, @NotNull Class<?> descendant) {
        return ancestor == descendant || ancestor.isAssignableFrom(descendant);
    }

    @NotNull
    public static JBTreeTraverser<Class> classTraverser(@Nullable Class root) {
        return CLASS_TRAVERSER.unique().withRoot(root);
    }

    private static final JBTreeTraverser<Class> CLASS_TRAVERSER = JBTreeTraverser.from(new Function<Class, Iterable<Class>>() {
        @Override
        public Iterable<Class> fun(Class aClass) {
            return JBIterable.of(aClass.getSuperclass()).append(aClass.getInterfaces());
        }
    });
}